spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { EventDTO, FacilityDetail } from "@dci/core";2import { ExternalLink } from "lucide-react";3import type { Metadata } from "next";4import Link from "next/link";5import type { ReactNode } from "react";6import { Breadcrumbs, type Crumb } from "@/components/entity/breadcrumbs";7import { EntityMap } from "@/components/entity/entity-map";8import { JsonLd } from "@/components/entity/json-ld";9import { ExtLink, KeyValueList, NotPublished } from "@/components/entity/kv";10import { ProvenanceTable, WithProvenance, provFor } from "@/components/entity/provenance";11import { SectionNav } from "@/components/entity/section-nav";12import { SourcesFooter } from "@/components/entity/sources-footer";13import { EventList } from "@/components/event-list";14import { CapacityHistoryChart, ClaimsTable, DataQualityPanel, Evidence, EvidenceProvider, NearbyInfrastructurePanel } from "@/components/evidence";15import { FacilityTable } from "@/components/facility-table";16import { ProjectList } from "@/components/project-list";17import { AiMark, Badge, ConfidenceBadge, EstimateMark, FlagBadges, PrecisionBadge, ScopeBadge, SemanticsLabel, StatusBadge, TypeBadge } from "@/components/ui/badge";18import { Page, Section } from "@/components/ui/card";19import { CountryFlag } from "@/components/ui/country-flag";20import { ProvenancePopover } from "@/components/ui/popover";21import { SourceBadge } from "@/components/ui/source-badge";22import { ErrorState, OfflineNotice } from "@/components/ui/states";23import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";24import { SourceHistory, TimelineList, type TimelineEntry } from "@/components/ui/timeline";25import { countryHref } from "@/lib/countries";26import { bestMw, fmtDate, fmtInt, fmtMw, fmtPct, placeLine, splitMw } from "@/lib/format";27import { CONFIDENCE_LABEL, EVENT_LABEL, PRECISION_LABEL, STATUS_LABEL, TYPE_LABEL, isImprecise } from "@/lib/labels";28import { routes } from "@/lib/routes";29import { NOT_FOUND_METADATA, absoluteUrl, compactJsonLd, pageMetadata } from "@/lib/seo";30import { getFacility, mwIsEstimate } from "./data";3132export const revalidate = 300;3334type Params = { params: Promise<{ slug: string }> };3536function describe(f: FacilityDetail): string {37 const { mw, kind } = bestMw(f);38 const parts = [`${f.name} — ${TYPE_LABEL[f.facilityType].toLowerCase()} data center${f.recordScope === "campus" ? " campus" : ""}`];39 if (f.operator) parts[0] += ` operated by ${f.operator.name}`;40 const place = placeLine(f);41 if (place !== "—") parts[0] += ` in ${place}`;42 parts.push(`Status: ${STATUS_LABEL[f.status].toLowerCase()}.`);43 if (mw !== null) parts.push(`${kind === "it" ? "Known IT capacity" : kind === "total" ? "Total power" : "Planned build-out"}: ${mwIsEstimate(f) ? "≈ " : ""}${fmtMw(mw)} (published figure).`);44 if (f.openedOn) parts.push(`Opened ${fmtDate(f.openedOn)}.`);45 parts.push("Every figure sourced, versioned and tracked on DataCenterIndex.");46 return parts.join(" ");47}4849export async function generateMetadata({ params }: Params): Promise<Metadata> {50 const { slug } = await params;51 const r = await getFacility(slug);52 if (!r.ok) return r.status === 404 ? NOT_FOUND_METADATA : { title: "Data center", robots: { index: false } };53 const f = r.data;54 const suffix = f.city ? ` (${f.city}${f.countryIso2 ? `, ${f.countryIso2}` : ""})` : "";55 return pageMetadata({ title: `${f.name}${suffix}`, description: describe(f), path: routes.facility(f.slug) });56}5758/* ------------------------------------------------------------------ helpers */5960const EXTERNAL_ID_LINKS: Record<string, { label: string; href: (v: string) => string }> = {61 wikidata: { label: "Wikidata", href: (v) => `https://www.wikidata.org/wiki/${v}` },62 wikipedia_en: { label: "Wikipedia", href: (v) => (v.startsWith("http") ? v : `https://en.wikipedia.org/wiki/${v}`) },63 peeringdb_fac: { label: "PeeringDB facility", href: (v) => `https://www.peeringdb.com/fac/${v}` },64 peeringdb: { label: "PeeringDB", href: (v) => `https://www.peeringdb.com/fac/${v}` },65 osm: { label: "OpenStreetMap", href: (v) => (v.startsWith("http") ? v : `https://www.openstreetmap.org/${v.replace(/^([nwr])(\d+)$/, (_, t: string, id: string) => `${t === "n" ? "node" : t === "w" ? "way" : "relation"}/${id}`)}`) },66 osm_way: { label: "OpenStreetMap way", href: (v) => `https://www.openstreetmap.org/way/${v}` },67 osm_node: { label: "OpenStreetMap node", href: (v) => `https://www.openstreetmap.org/node/${v}` },68};6970function externalIdRows(ids: Record<string, string | number>) {71 return Object.entries(ids)72 .filter(([, v]) => v !== null && v !== undefined && v !== "")73 .map(([k, v]) => {74 const spec = EXTERNAL_ID_LINKS[k];75 const value = String(v);76 const label = spec?.label ?? k.replace(/_/g, " ");77 return { label, value: spec ? <ExtLink href={spec.href(value)}>{value.startsWith("http") ? decodeURIComponent(value.split("/").filter(Boolean).pop() ?? value).replace(/_/g, " ") : value}</ExtLink> : <span className="figure text-[12.5px]">{value}</span> };78 });79}8081function completenessTone(c: number): string {82 return c >= 70 ? "bg-ok" : c >= 40 ? "bg-ink-3" : "bg-warn";83}8485/** One header figure: caps label, big value (or badge), note line. */86function HeaderFigure({ label, children, note, className }: { label: ReactNode; children: ReactNode; note?: ReactNode; className?: string }) {87 return (88 <div className={`min-w-0 px-3 py-3 sm:px-4 sm:first:pl-0 ${className ?? ""}`}>89 <div className="label-caps mb-1 truncate">{label}</div>90 <div className="min-w-0">{children}</div>91 {note && <div className="mt-1 text-[11px] leading-tight text-ink-3">{note}</div>}92 </div>93 );94}9596/** MW figure for the header: value + unit in mono, estimate mark, semantics + scope underneath; honest "Not published". */97function MwFigure({ field, aliases, mw, estimate, semantics, scope, planned, fallbackNote }: { field: string; aliases?: string[]; mw: number | null; estimate?: boolean; semantics: string; scope?: string | null; planned?: boolean; fallbackNote: string }) {98 const s = splitMw(mw);99 if (!s) {100 return (101 <Evidence field={field} fields={aliases} display="—" block>102 <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink-4 sm:text-[26px]">—</div>103 <div className="mt-1 text-[11px] leading-tight text-ink-3">{fallbackNote}</div>104 </Evidence>105 );106 }107 return (108 <Evidence field={field} fields={aliases} display={`${estimate || planned ? "≈ " : ""}${fmtMw(mw)}`} block>109 <div className="flex items-baseline gap-1 leading-none">110 {(estimate || planned) && <EstimateMark className="text-[14px]" />}111 <span className="figure text-[22px] font-medium tracking-[-0.03em] text-ink sm:text-[26px]">{s.value}</span>112 <span className="text-[12px] font-medium text-ink-3">{s.unit}</span>113 </div>114 <div className="mt-1 flex flex-wrap items-center gap-1.5">115 <SemanticsLabel semantics={semantics} />116 {scope && <ScopeBadge scope={scope} />}117 {estimate && <span className="text-[11px] text-ink-3">estimate</span>}118 </div>119 </Evidence>120 );121}122123function eventDate(e: EventDTO): string {124 return e.effectiveDate ?? e.detectedAt;125}126127/* ------------------------------------------------------------------ page */128129export default async function FacilityPage({ params }: Params) {130 const { slug } = await params;131 const r = await getFacility(slug);132133 if (!r.ok) {134 return (135 <Page className="py-6">136 <Breadcrumbs items={[{ name: "Data centers", href: routes.datacenters() }, { name: slug, href: routes.facility(slug) }]} />137 <OfflineNotice detail={r.error} className="mt-4" />138 <ErrorState className="mt-4" detail={r.error} />139 </Page>140 );141 }142143 const f = r.data;144 const prov = f.provenance ?? [];145 const claims = f.claims ?? [];146 const history = f.capacityHistory ?? [];147 const est = mwIsEstimate(f);148 const hasGeo = f.lat !== null && f.lng !== null;149 const imprecise = isImprecise(f.geoPrecision);150 const cHref = await countryHref(f.countryIso2, f.countryName);151 const place = placeLine(f);152 const best = bestMw(f);153 const isCampus = f.recordScope === "campus" || f.buildings.length > 0;154 const parent = f.campus ?? f.parentFacility ?? null;155 const power = f.powerContext;156 const dq = f.dataQuality;157158 const crumbs: Crumb[] = [{ name: "Data centers", href: routes.datacenters() }];159 if (f.metro) crumbs.push({ name: f.metro.name, href: routes.market(f.metro.slug) });160 else if (cHref && f.countryName) crumbs.push({ name: f.countryName, href: cHref });161 if (parent) crumbs.push({ name: parent.name, href: routes.facility(parent.slug) });162 crumbs.push({ name: f.name, href: routes.facility(f.slug) });163164 const connectivityEmpty = !f.carriers.length && !f.cloudProviders.length && !f.ixps.length && !f.carriersCount && !f.ixpCount && !f.networksCount;165 const powerEmpty = f.utilityCapacityMw == null && f.gridConnectionMw == null && !power?.powerEvents.length && !power?.gridConstraints.length;166 const tenants = f.tenants.filter((t) => t.role !== "carrier");167 const carrierTenants = f.tenants.filter((t) => t.role === "carrier");168169 // timeline: dated milestones + status / capacity events with an effective date170 const timeline: TimelineEntry[] = [];171 if (f.announcedOn) timeline.push({ date: f.announcedOn, title: "Announced", tone: "accent", meta: <WithProvenance items={prov} fields={["announcedOn", "announced_on"]}>as published</WithProvenance> });172 if (f.constructionStartedOn) timeline.push({ date: f.constructionStartedOn, title: "Construction started", tone: "warn", meta: <WithProvenance items={prov} fields={["constructionStartedOn", "construction_started_on"]}>as published</WithProvenance> });173 if (f.openedOn) timeline.push({ date: f.openedOn, title: "Opened", tone: "ok", meta: <WithProvenance items={prov} fields={["openedOn", "opened_on"]}>as published</WithProvenance> });174 for (const e of f.events.filter((e) => ["status_changed", "capacity_changed", "planned_capacity_changed", "opening_date_changed", "operator_changed", "owner_changed", "expansion_announced", "closure"].includes(e.eventType)).slice(0, 20)) {175 timeline.push({ date: eventDate(e), title: EVENT_LABEL[e.eventType], description: e.title !== f.name ? e.title : undefined, meta: <span>{e.sourceName}</span>, href: e.url, tone: e.eventType === "closure" ? "danger" : "neutral" });176 }177 timeline.push({ date: f.firstSeen, title: <span className="text-ink-2">First indexed</span>, tone: "neutral", meta: <span>{fmtInt(f.sourceCount ?? dq.sourceCount)} sources since</span> });178 if (f.lastVerified) timeline.push({ date: f.lastVerified, title: <span className="text-ink-2">Last verified against a primary source</span>, tone: "ok" });179 timeline.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));180181 const sections = [182 { id: "overview", label: "Overview" },183 { id: "capacity", label: "Capacity" },184 { id: "timeline", label: "Timeline", count: timeline.length },185 { id: "ownership", label: "Operator / ownership" },186 { id: "connectivity", label: "Connectivity" },187 { id: "power", label: "Power" },188 { id: "cloud", label: "Cloud / tenants" },189 { id: "phases", label: "Project phases", count: f.projects.length },190 { id: isCampus ? "buildings" : "campus", label: isCampus ? "Buildings" : "Campus", count: isCampus ? f.buildings.length : parent ? 1 : 0 },191 { id: "nearby", label: "Nearby", count: hasGeo ? 1 : 0 },192 { id: "events", label: "Event history", count: f.events.length },193 { id: "evidence", label: "Source evidence", count: prov.length + claims.length },194 { id: "quality", label: "Data quality" },195 ];196197 const jsonLd = compactJsonLd({198 "@context": "https://schema.org",199 "@type": "Place",200 "@id": `${absoluteUrl(routes.facility(f.slug))}#place`,201 name: f.name,202 alternateName: f.aliases.length ? f.aliases : undefined,203 description: f.description ?? undefined,204 url: absoluteUrl(routes.facility(f.slug)),205 sameAs: [f.website, ...Object.entries(f.externalIds).map(([k, v]) => (EXTERNAL_ID_LINKS[k] ? EXTERNAL_ID_LINKS[k].href(String(v)) : null))].filter(Boolean),206 address: compactJsonLd({ "@type": "PostalAddress", streetAddress: f.address ?? undefined, addressLocality: f.city ?? undefined, addressRegion: f.regionName ?? undefined, postalCode: f.postalCode ?? undefined, addressCountry: f.countryIso2 ?? undefined }),207 geo: hasGeo && f.geoPrecision !== "unknown" ? { "@type": "GeoCoordinates", latitude: f.lat, longitude: f.lng } : undefined,208 containedInPlace: parent ? { "@type": "Place", name: parent.name, url: absoluteUrl(routes.facility(parent.slug)) } : f.metro ? { "@type": "Place", name: f.metro.name, url: absoluteUrl(routes.market(f.metro.slug)) } : undefined,209 additionalProperty: [210 { "@type": "PropertyValue", name: "status", value: STATUS_LABEL[f.status] },211 { "@type": "PropertyValue", name: "facilityType", value: TYPE_LABEL[f.facilityType] },212 f.itCapacityMw !== null ? { "@type": "PropertyValue", name: "IT capacity", value: f.itCapacityMw, unitText: "MW" } : null,213 f.totalPowerMw !== null ? { "@type": "PropertyValue", name: "Total power", value: f.totalPowerMw, unitText: "MW" } : null,214 f.plannedPowerMw !== null ? { "@type": "PropertyValue", name: "Planned power", value: f.plannedPowerMw, unitText: "MW" } : null,215 { "@type": "PropertyValue", name: "geoPrecision", value: PRECISION_LABEL[f.geoPrecision] },216 ].filter(Boolean),217 });218219 const opLink = (o: { slug: string; name: string } | null | undefined, empty: string) =>220 o ? (221 <Link href={routes.operator(o.slug)} className="text-ink hover:text-accent hover:underline">222 {o.name}223 </Link>224 ) : (225 <span className="text-[12.5px] text-ink-3">{empty}</span>226 );227228 return (229 <EvidenceProvider data={{ entityName: f.name, entityKind: "facility", claims, provenance: prov, history }}>230 <Page className="pb-8">231 <JsonLd data={jsonLd} />232 <div className="pt-3">233 <Breadcrumbs items={crumbs} />234 </div>235236 {/* ---------------------------------------------------------------- header */}237 <header className="py-4 sm:py-6">238 <div className="label-caps mb-1.5 flex flex-wrap items-center gap-x-2 gap-y-1">239 <span>{isCampus ? "Data center campus" : f.recordScope === "building" ? "Data center building" : "Data center"}</span>240 {f.operator && (241 <>242 <span aria-hidden className="text-ink-4">·</span>243 <Link href={routes.operator(f.operator.slug)} className="normal-case tracking-normal text-[12.5px] font-medium text-ink-2 hover:text-accent hover:underline">244 {f.operator.name}245 </Link>246 </>247 )}248 {parent && (249 <>250 <span aria-hidden className="text-ink-4">·</span>251 <span className="normal-case tracking-normal text-[12px] text-ink-3">252 part of{" "}253 <Link href={routes.facility(parent.slug)} className="text-ink-2 hover:text-accent hover:underline">254 {parent.name}255 </Link>256 </span>257 </>258 )}259 </div>260 <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[30px]">261 <WithProvenance items={prov} fields="name">262 {f.name}263 </WithProvenance>264 </h1>265 {place !== "—" && (266 <p className="mt-1 flex flex-wrap items-center gap-x-1.5 text-[13.5px] text-ink-2">267 <CountryFlag iso2={f.countryIso2} size={13} />268 {f.city && <span>{f.city}</span>}269 {f.metro && (270 <>271 <span aria-hidden className="text-ink-4">·</span>272 <Link href={routes.market(f.metro.slug)} className="hover:text-accent hover:underline">273 {f.metro.name} market274 </Link>275 </>276 )}277 {f.countryName && (278 <>279 <span aria-hidden className="text-ink-4">·</span>280 {cHref ? (281 <Link href={cHref} className="hover:text-accent hover:underline">282 {f.countryName}283 </Link>284 ) : (285 <span>{f.countryName}</span>286 )}287 </>288 )}289 </p>290 )}291 <div className="mt-3 flex flex-wrap items-center gap-1.5">292 <TypeBadge type={f.facilityType} />293 {f.isAi ? <AiMark evidence={f.aiEvidence} size="sm" /> : null}294 <FlagBadges isHyperscale={f.isHyperscale} size="sm" />295 {f.recordScope && <ScopeBadge scope={f.recordScope} size="sm" short={false} />}296 <span className="inline-flex h-[22px] items-center gap-1.5 rounded-xs border border-hair-2 px-2 text-[11.5px] text-ink-2" title={`Completeness ${f.completeness}/100 — share of tracked fields with a sourced value`}>297 <span className="inline-block h-[4px] w-10 overflow-hidden rounded-[1px] bg-surface-3" aria-hidden>298 <span className={`block h-full ${completenessTone(f.completeness)}`} style={{ width: `${Math.max(2, Math.min(100, f.completeness))}%` }} />299 </span>300 <span className="figure">{f.completeness}%</span>301 <span className="text-ink-3">complete</span>302 </span>303 </div>304 {f.aliases.length > 0 && (305 <p className="mt-2 text-[12px] text-ink-3">306 Also known as <span className="text-ink-2">{f.aliases.slice(0, 6).join(", ")}</span>307 {f.aliases.length > 6 && ` and ${f.aliases.length - 6} more`}308 </p>309 )}310 </header>311312 {/* ---------------------------------------------------------------- header figures (each opens the evidence drawer) */}313 <div className="grid grid-cols-2 border-y border-hair [&>*]:min-w-0 max-sm:[&>*:nth-child(even)]:border-l max-sm:[&>*:nth-child(n+3)]:border-t max-sm:[&>*:nth-child(odd)]:pl-0 max-sm:[&>*:last-child:nth-child(odd)]:col-span-2 max-sm:[&>*]:border-hair sm:grid-cols-4 sm:[&>*:nth-child(n+5)]:border-t sm:[&>*:nth-child(4n+1)]:pl-0 sm:[&>*:not(:nth-child(4n+1))]:border-l sm:[&>*]:border-hair xl:grid-cols-7 xl:[&>*:nth-child(n+5)]:border-t-0 xl:[&>*:nth-child(4n+1)]:pl-4 xl:[&>*:first-child]:pl-0 xl:[&>*+*]:border-l">314 <HeaderFigure label="Status">315 <Evidence field="status" display={STATUS_LABEL[f.status]}>316 <StatusBadge status={f.status} />317 </Evidence>318 {f.openedOn && <div className="figure mt-1 text-[11px] text-ink-3">opened {fmtDate(f.openedOn)}</div>}319 </HeaderFigure>320 <HeaderFigure label="Known IT capacity">321 <MwFigure field="itCapacityMw" aliases={["it_capacity_mw", "critical_power_mw"]} mw={f.itCapacityMw} estimate={est && best.kind === "it"} semantics={f.capacitySemantics && best.kind === "it" ? f.capacitySemantics : "it_capacity_mw"} scope={best.kind === "it" ? f.capacityScope : null} fallbackNote="Not published by any source" />322 </HeaderFigure>323 <HeaderFigure label="Total power">324 <MwFigure field="totalPowerMw" aliases={["total_power_mw", "current_power_mw"]} mw={f.totalPowerMw} estimate={est && best.kind === "total"} semantics={f.capacitySemantics && best.kind === "total" ? f.capacitySemantics : "current_power_mw"} scope={best.kind === "total" ? f.capacityScope : null} fallbackNote="Not published by any source" />325 </HeaderFigure>326 <HeaderFigure label="Planned build-out">327 <MwFigure field="plannedPowerMw" aliases={["planned_power_mw", "phase_mw", "plannedMw"]} mw={f.plannedPowerMw ?? f.ultimateCampusMw ?? null} planned semantics={f.plannedPowerMw !== null ? "planned_power_mw" : "ultimate_campus_mw"} scope={best.kind === "planned" ? f.capacityScope : null} fallbackNote={f.status === "operational" ? "No expansion figure published" : "Not published by any source"} />328 </HeaderFigure>329 <HeaderFigure label="Confidence" note={`${fmtInt(dq.sourceCount)} ${dq.sourceCount === 1 ? "source" : "sources"} · ${fmtInt(dq.primarySourceCount)} primary`}>330 <Evidence field="confidence" fields={["status", "itCapacityMw", "totalPowerMw"]} display={CONFIDENCE_LABEL[f.confidence]}>331 <ConfidenceBadge level={f.confidence} short={false} />332 </Evidence>333 </HeaderFigure>334 <HeaderFigure label="Location precision" note={hasGeo ? (imprecise ? "ring marks the area, not the building" : `${f.lat!.toFixed(4)}, ${f.lng!.toFixed(4)}`) : "no coordinates published"}>335 <Evidence field="geoPrecision" fields={["geo_precision", "lat", "lng", "geo", "coordinates"]} display={PRECISION_LABEL[f.geoPrecision]}>336 <PrecisionBadge precision={f.geoPrecision} short={false} />337 </Evidence>338 </HeaderFigure>339 <HeaderFigure label="Last verified" note={f.lastVerified ? "against a primary source" : "no primary-source verification yet"}>340 <Evidence field="lastVerified" fields={["last_verified"]} display={f.lastVerified ? fmtDate(f.lastVerified) : "—"} block>341 <div className={`figure text-[18px] font-medium leading-none tracking-[-0.02em] ${f.lastVerified ? "text-ink" : "text-ink-4"}`}>{f.lastVerified ? fmtDate(f.lastVerified) : "—"}</div>342 </Evidence>343 </HeaderFigure>344 </div>345 <p className="mt-2 text-[11px] text-ink-4">Figures are published values only (≈ marks estimates and planned figures); click any figure for its sources, sentence-level evidence and history. Nothing is imputed.</p>346347 <div className="mt-4">348 <SectionNav items={sections} />349 </div>350351 <div className="flex flex-col gap-9 pt-6">352 {/* ------------------------------------------------------------ overview */}353 <Section id="overview" title="Overview" className="scroll-mt-28">354 <div className="grid gap-6 md:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)]">355 <div className="min-w-0">356 {f.description ? (357 <p className="text-[14px] leading-relaxed text-ink-2">358 <WithProvenance items={prov} fields="description">359 <span>{f.description}</span>360 </WithProvenance>361 </p>362 ) : (363 <NotPublished what="Description" />364 )}365 <KeyValueList366 className="mt-4"367 columns={2}368 items={[369 { label: "Type", value: <TypeBadge type={f.facilityType} size="xs" /> },370 { label: "Tier", value: f.tier ? <WithProvenance items={prov} fields="tier">{f.tier}</WithProvenance> : null },371 { label: "Building", value: f.buildingSqm !== null ? <Evidence field="buildingSqm" fields={["building_sqm"]}><span className="figure">{fmtInt(f.buildingSqm)} m²</span></Evidence> : null },372 { label: "Site area", value: f.siteAreaHa !== null ? <Evidence field="siteAreaHa" fields={["site_area_ha"]}><span className="figure">{fmtInt(f.siteAreaHa)} ha</span></Evidence> : null },373 { label: "Racks", value: f.rackCount !== null ? <Evidence field="rackCount" fields={["rack_count"]}><span className="figure">{fmtInt(f.rackCount)}</span></Evidence> : null },374 { label: "PUE", value: f.pue !== null ? <Evidence field="pue"><span className="figure">{f.pue.toFixed(2)}</span></Evidence> : null },375 { label: "Cooling", value: f.coolingType ? <WithProvenance items={prov} fields={["coolingType", "cooling_type"]}>{f.coolingType}</WithProvenance> : null },376 { label: "Renewable claim", value: f.renewableClaim ? <WithProvenance items={prov} fields={["renewableClaim", "renewable_claim"]}>{f.renewableClaim}</WithProvenance> : null },377 { label: "Website", value: f.website ? <WithProvenance items={prov} fields="website"><ExtLink href={f.website} /></WithProvenance> : null },378 ...externalIdRows(f.externalIds),379 { label: "First indexed", value: <span className="figure text-[12.5px] text-ink-2">{fmtDate(f.firstSeen)}</span> },380 { label: "Record updated", value: <span className="figure text-[12.5px] text-ink-2">{fmtDate(f.updatedAt)}</span> },381 ]}382 />383 {f.certifications.length > 0 && (384 <div className="mt-4">385 <div className="label-caps mb-1.5 flex items-center gap-1">386 Certifications <ProvenancePopover items={provFor(prov, "certifications")} size={12} />387 </div>388 <div className="flex flex-wrap gap-1.5">389 {f.certifications.map((c) => (390 <Badge key={c} tone="outline">391 {c}392 </Badge>393 ))}394 </div>395 </div>396 )}397 </div>398 <div className="min-w-0">399 {hasGeo ? (400 <EntityMap center={{ lat: f.lat!, lng: f.lng! }} precision={f.geoPrecision} filters={f.countryIso2 ? { country: f.countryIso2 } : undefined} className="h-[200px] sm:h-[260px]" openLabel="Open in map" />401 ) : (402 <div className="flex min-h-[140px] items-center justify-center rounded-sm border border-dashed border-hair-2 text-[12.5px] text-ink-3">No coordinates published — not drawn on the map.</div>403 )}404 <KeyValueList405 className="mt-3"406 items={[407 { label: "Address", value: f.address ? <WithProvenance items={prov} fields="address">{f.address}</WithProvenance> : null },408 { label: "Postal code", value: f.postalCode ? <span className="figure">{f.postalCode}</span> : null },409 { label: "City", value: f.city ? <WithProvenance items={prov} fields="city">{f.city}</WithProvenance> : null },410 { label: "Region", value: f.regionName ? <WithProvenance items={prov} fields={["regionName", "region_name"]}>{f.regionName}</WithProvenance> : null },411 { label: "Market", value: f.metro ? <Link href={routes.market(f.metro.slug)} className="hover:text-accent hover:underline">{f.metro.name}</Link> : null },412 { label: "Country", value: f.countryIso2 ? <span className="inline-flex items-center gap-1.5"><CountryFlag iso2={f.countryIso2} size={13} />{cHref ? <Link href={cHref} className="hover:text-accent hover:underline">{f.countryName ?? f.countryIso2}</Link> : <span>{f.countryName ?? f.countryIso2}</span>}</span> : null },413 { label: "Coordinates", value: hasGeo ? <Evidence field="lat" fields={["lng", "geo", "coordinates", "geoPrecision"]}><span className="figure text-[12.5px]">{f.lat!.toFixed(4)}, {f.lng!.toFixed(4)}</span></Evidence> : null },414 { label: "Geo source", value: f.geoSource ? <span className="figure text-[12px] text-ink-3">{f.geoSource}</span> : null },415 ]}416 />417 {!f.address && !hasGeo && <NotPublished what="Address and coordinates" className="mt-2" />}418 {hasGeo && imprecise && <p className="mt-2 text-[12px] text-ink-3">Location shown at {PRECISION_LABEL[f.geoPrecision].toLowerCase().replace(" location", "")}: the exact site has not been published by the indexed sources; the ring marks the area, not the building.</p>}419 </div>420 </div>421 </Section>422423 {/* ------------------------------------------------------------ capacity */}424 <Section id="capacity" title="Capacity" className="scroll-mt-28" description="Every capacity figure published about this record, by what it measures (IT load ≠ total power ≠ planned ≠ grid connection), its scope and its source. Only site-scoped figures populate the columns above.">425 <div className="grid gap-6 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">426 <div className="min-w-0">427 <div className="label-caps mb-2">Capacity claims</div>428 <ClaimsTable claims={claims} kind="capacity" emptyNote={best.mw !== null ? "No sentence-level capacity claim yet — the figures above come from structured source fields listed under Source evidence." : "No source has published a capacity figure for this record."} />429 {(f.utilityCapacityMw != null || f.gridConnectionMw != null || f.ultimateCampusMw != null) && (430 <KeyValueList431 className="mt-4"432 items={[433 { label: "Utility capacity", value: f.utilityCapacityMw != null ? <Evidence field="utilityCapacityMw"><span className="figure">{fmtMw(f.utilityCapacityMw)}</span> <SemanticsLabel semantics="utility_capacity_mw" /></Evidence> : null },434 { label: "Grid connection", value: f.gridConnectionMw != null ? <Evidence field="gridConnectionMw"><span className="figure">{fmtMw(f.gridConnectionMw)}</span> <SemanticsLabel semantics="grid_connection_mw" /></Evidence> : null },435 { label: "Ultimate build-out", value: f.ultimateCampusMw != null ? <Evidence field="ultimateCampusMw"><EstimateMark className="mr-0.5" /><span className="figure">{fmtMw(f.ultimateCampusMw)}</span> <SemanticsLabel semantics="ultimate_campus_mw" /></Evidence> : null },436 ]}437 />438 )}439 </div>440 <div className="min-w-0">441 <div className="label-caps mb-2">Capacity history</div>442 <CapacityHistoryChart points={history} height={170} />443 </div>444 </div>445 </Section>446447 {/* ------------------------------------------------------------ timeline */}448 <Section id="timeline" title="Timeline" className="scroll-mt-28" description="Dated milestones and detected changes, oldest first. Partial dates are shown as published.">449 <TimelineList items={timeline} dense />450 </Section>451452 {/* ------------------------------------------------------------ operator / ownership */}453 <Section id="ownership" title="Operator / ownership" className="scroll-mt-28" description="Operator, owner, developer, landowner and tenants are distinct roles and are never merged.">454 <KeyValueList455 columns={2}456 items={[457 { label: "Operator", value: <WithProvenance items={prov} fields={["operator", "operatorName"]}>{opLink(f.operator, "Not published")}</WithProvenance>, keepEmpty: true },458 { label: "Owner", value: f.owner ? <WithProvenance items={prov} fields={["owner", "ownerName"]}>{opLink(f.owner, "")}{f.owner.id === f.operator?.id && <span className="ml-1.5 text-[11.5px] text-ink-3">(same as operator)</span>}</WithProvenance> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },459 { label: "Developer", value: opLink(f.developer, "Not published"), keepEmpty: true },460 { label: "Landowner", value: opLink(f.landowner, "Not published"), keepEmpty: true },461 {462 label: "Tenants",463 value: tenants.length ? (464 <ul className="flex flex-wrap gap-1.5">465 {tenants.map((t) => (466 <li key={t.id}>467 <Link href={routes.operator(t.slug)} className="inline-flex h-7 items-center gap-1.5 rounded-xs border border-hair px-2 text-[12px] text-ink hover:text-accent">468 {t.name} <span className="text-ink-4">{t.role}</span>469 </Link>470 </li>471 ))}472 </ul>473 ) : (474 <span className="text-[12.5px] text-ink-3">No tenant published{carrierTenants.length ? ` (carriers are listed under Connectivity)` : ""}</span>475 ),476 keepEmpty: true,477 },478 ]}479 />480 {f.events.some((e) => e.eventType === "operator_changed" || e.eventType === "owner_changed" || e.eventType === "acquisition") && (481 <div className="mt-4">482 <div className="label-caps mb-1.5">Ownership changes</div>483 <EventList events={f.events.filter((e) => e.eventType === "operator_changed" || e.eventType === "owner_changed" || e.eventType === "acquisition")} dense />484 </div>485 )}486 </Section>487488 {/* ------------------------------------------------------------ connectivity */}489 <Section id="connectivity" title="Connectivity" className="scroll-mt-28" description="Carriers, cloud on-ramps and internet exchanges present at this site, as published by the operator or PeeringDB.">490 {connectivityEmpty ? (491 <NotPublished what="Connectivity (carriers, cloud on-ramps, IXPs)" />492 ) : (493 <div className="grid gap-4 sm:grid-cols-3">494 <div>495 <div className="label-caps mb-1.5">496 Carriers {f.carriersCount !== null && f.carriersCount !== undefined && <span className="figure text-ink-3">{fmtInt(f.carriersCount)}</span>}497 {f.networksCount !== null && f.networksCount !== undefined && <span className="ml-2 normal-case tracking-normal text-ink-4">{fmtInt(f.networksCount)} networks</span>}498 </div>499 {f.carriers.length ? (500 <ul className="divide-y divide-hair text-[13px]">501 {f.carriers.map((c) => (502 <li key={c.id} className="flex items-baseline justify-between gap-2 py-1.5">503 <Link href={routes.operator(c.slug)} className="truncate text-ink hover:text-accent hover:underline">504 {c.name}505 </Link>506 {c.asn && <span className="figure shrink-0 text-[11.5px] text-ink-3">AS{c.asn}</span>}507 </li>508 ))}509 </ul>510 ) : carrierTenants.length ? (511 <ul className="divide-y divide-hair text-[13px]">512 {carrierTenants.map((c) => (513 <li key={c.id} className="py-1.5">514 <Link href={routes.operator(c.slug)} className="text-ink hover:text-accent hover:underline">515 {c.name}516 </Link>517 </li>518 ))}519 </ul>520 ) : (521 <p className="text-[12.5px] text-ink-3">{f.carriersCount ? "Names not published; count from PeeringDB." : "None listed."}</p>522 )}523 </div>524 <div>525 <div className="label-caps mb-1.5">Cloud on-ramps</div>526 {f.cloudProviders.length ? (527 <ul className="divide-y divide-hair text-[13px]">528 {f.cloudProviders.map((c) => (529 <li key={c.id} className="py-1.5">530 <Link href={routes.operator(c.slug)} className="text-ink hover:text-accent hover:underline">531 {c.name}532 </Link>533 </li>534 ))}535 </ul>536 ) : (537 <p className="text-[12.5px] text-ink-3">None listed.</p>538 )}539 </div>540 <div>541 <div className="label-caps mb-1.5">542 Internet exchanges {f.ixpCount !== null && f.ixpCount !== undefined && <span className="figure text-ink-3">{fmtInt(f.ixpCount)}</span>}543 </div>544 {f.ixps.length ? (545 <ul className="divide-y divide-hair text-[13px]">546 {f.ixps.map((x) => (547 <li key={x.id} className="py-1.5">548 <Link href={routes.ixp(x.slug)} className="text-ink hover:text-accent hover:underline">549 {x.name}550 </Link>551 </li>552 ))}553 </ul>554 ) : (555 <p className="text-[12.5px] text-ink-3">{f.ixpCount ? "Names not published; count from PeeringDB." : "None listed."}</p>556 )}557 </div>558 </div>559 )}560 </Section>561562 {/* ------------------------------------------------------------ power */}563 <Section id="power" title="Power" className="scroll-mt-28" description="Utility-side figures (power available or contracted from the grid) are never compared with IT load. Grid events and constraints are attached to the market or country, not to the site.">564 <div className="grid gap-6 md:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">565 <div className="min-w-0">566 <KeyValueList567 items={[568 { label: "Utility capacity", value: f.utilityCapacityMw != null ? <Evidence field="utilityCapacityMw"><span className="figure">{fmtMw(f.utilityCapacityMw)}</span></Evidence> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },569 { label: "Grid connection", value: f.gridConnectionMw != null ? <Evidence field="gridConnectionMw"><span className="figure">{fmtMw(f.gridConnectionMw)}</span></Evidence> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },570 { label: "Renewable claim", value: f.renewableClaim ?? <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },571 ]}572 />573 {power?.countryEnergy && (f.countryName || f.countryIso2) && (574 <div className="mt-4">575 <div className="label-caps mb-1.5">Country energy context · {f.countryName ?? f.countryIso2}</div>576 <div className="grid grid-cols-2 gap-3">577 <div>578 <div className="figure text-[18px] font-medium text-ink">{power.countryEnergy.renewableShare !== null ? fmtPct(power.countryEnergy.renewableShare / 100) : "—"}</div>579 <div className="text-[11px] text-ink-3">renewable share of generation{power.countryEnergy.statsYear ? ` · ${power.countryEnergy.statsYear}` : ""}</div>580 </div>581 <div>582 <div className="figure text-[18px] font-medium text-ink">{power.countryEnergy.electricityTwh !== null ? `${fmtInt(power.countryEnergy.electricityTwh)} TWh` : "—"}</div>583 <div className="text-[11px] text-ink-3">national generation{power.countryEnergy.statsYear ? ` · ${power.countryEnergy.statsYear}` : ""}</div>584 </div>585 </div>586 <p className="mt-2 text-[11px] text-ink-4">National grid averages — they do not describe this facility's contracted electricity or power purchase agreements.</p>587 </div>588 )}589 </div>590 <div className="min-w-0">591 <div className="label-caps mb-1.5">Power events</div>592 {power?.powerEvents.length ? <EventList events={power.powerEvents} dense /> : <p className="text-[12.5px] text-ink-3">No power agreement or utility event linked to this facility.</p>}593 <div className="label-caps mb-1.5 mt-4">Grid constraints in this market</div>594 {power?.gridConstraints.length ? (595 <ul className="divide-y divide-hair">596 {power.gridConstraints.map((g) => (597 <li key={g.id} className="py-2 text-[13px]">598 <div className="flex flex-wrap items-center gap-1.5">599 <Badge tone="outline" size="xs">600 {g.kind.replace(/_/g, " ")}601 </Badge>602 {g.sourceKind && <SourceBadge kind={g.sourceKind} name={g.sourceName} url={g.url} />}603 <span className="figure ml-auto text-[11.5px] text-ink-3">{fmtDate(g.effectiveDate)}</span>604 </div>605 <a href={g.url} target="_blank" rel="noopener noreferrer" className="mt-1 inline-flex items-center gap-1 font-medium text-ink hover:text-accent">606 {g.title} <ExternalLink className="size-3" />607 </a>608 {g.summary && <p className="mt-0.5 text-[12.5px] text-ink-2">{g.summary}</p>}609 </li>610 ))}611 </ul>612 ) : (613 <p className="text-[12.5px] text-ink-3">No public grid constraint reported for this market.</p>614 )}615 {powerEmpty && power?.note && <p className="mt-3 text-[11px] text-ink-4">{power.note}</p>}616 </div>617 </div>618 </Section>619620 {/* ------------------------------------------------------------ cloud / tenants */}621 <Section id="cloud" title="Cloud / tenants" className="scroll-mt-28" description={f.cloudRegions.length ? "Public cloud regions published for the same metro. Providers publish the metro, not the building — presence here does not imply this facility hosts them." : "Cloud providers publish regions per metro, not per building."} aside={<Link href={routes.cloudRegions()} className="text-[12.5px] text-ink-3 hover:text-ink">All cloud regions →</Link>}>622 {f.cloudRegions.length ? (623 <Table dense>624 <THead>625 <tr>626 <Th>Provider</Th>627 <Th>Code</Th>628 <Th className="hidden sm:table-cell">Name</Th>629 <Th numeric className="hidden md:table-cell">630 AZs631 </Th>632 <Th className="hidden md:table-cell">Launched</Th>633 <Th>Status</Th>634 </tr>635 </THead>636 <TBody>637 {f.cloudRegions.map((cr) => (638 <Tr key={cr.id}>639 <Td primary>640 <Link href={routes.operator(cr.provider.slug)} className="font-medium text-ink hover:text-accent hover:underline">641 {cr.provider.name}642 </Link>643 <span className="ml-2 text-[12px] text-ink-3 sm:hidden">{cr.name}</span>644 </Td>645 <Td label="Code" className="figure text-[12.5px]">646 <RowLink href={routes.cloudRegion(cr.slug)} className="font-normal">647 {cr.code}648 </RowLink>649 </Td>650 <Td label="Name" className="hidden text-ink-2 sm:table-cell">651 {cr.name}652 </Td>653 <Td label="AZs" numeric className="hidden md:table-cell">654 {cr.availabilityZones ?? "—"}655 </Td>656 <Td label="Launched" className="figure hidden text-[12.5px] text-ink-3 md:table-cell">657 {fmtDate(cr.launchedOn)}658 </Td>659 <Td label="Status">660 <Badge tone={cr.status === "operational" ? "ok" : cr.status === "announced" ? "info" : cr.status === "restricted" ? "warn" : "neutral"} size="xs">661 {cr.status}662 </Badge>663 {cr.isSovereign && (664 <Badge tone="accent" size="xs" className="ml-1">665 Sovereign666 </Badge>667 )}668 </Td>669 </Tr>670 ))}671 </TBody>672 </Table>673 ) : (674 <p className="text-[12.5px] text-ink-3">No public cloud region is published for this market.</p>675 )}676 {tenants.length > 0 && (677 <p className="mt-3 text-[12.5px] text-ink-2">678 Published tenants: {tenants.map((t, i) => (679 <span key={t.id}>680 {i > 0 && ", "}681 <Link href={routes.operator(t.slug)} className="hover:text-accent hover:underline">682 {t.name}683 </Link>684 </span>685 ))}686 .687 </p>688 )}689 </Section>690691 {/* ------------------------------------------------------------ project phases */}692 <Section id="phases" title="Project phases" className="scroll-mt-28" description="Announced or under-construction phases linked to this facility, or to the same operator in this market. Planned MW are published figures (≈).">693 <ProjectList items={f.projects} emptyTitle="No project phase linked" emptyDescription="Expansion phases appear here when a source publishes them." />694 </Section>695696 {/* ------------------------------------------------------------ buildings / campus */}697 {isCampus ? (698 <Section id="buildings" title="Buildings" className="scroll-mt-28" description="Buildings contained in this campus. A campus whose buildings publish figures contributes no MW itself; totals are never double-counted." aside={<span className="figure text-[12px] text-ink-3">{fmtInt(f.buildings.length)}</span>}>699 <FacilityTable items={f.buildings} columns={["operator", "status", "mw", "type", "confidence"]} emptyTitle="No building published yet" emptyDescription="This record is a campus; individual buildings appear once a source publishes them." />700 </Section>701 ) : (702 <Section id="campus" title="Campus" className="scroll-mt-28" description="Containment: a building's figure never stands for its campus, and a campus figure is never written to a building.">703 {parent ? (704 <p className="text-[13.5px] text-ink-2">705 This {f.recordScope === "building" ? "building" : "facility"} is part of{" "}706 <Link href={routes.facility(parent.slug)} className="font-medium text-ink hover:text-accent hover:underline">707 {parent.name}708 </Link>709 .710 </p>711 ) : (712 <p className="text-[12.5px] text-ink-3">Not part of a published campus record.</p>713 )}714 </Section>715 )}716717 {/* ------------------------------------------------------------ nearby */}718 <Section id="nearby" title="Nearby infrastructure" className="scroll-mt-28" description="Great-circle distance between published coordinates. Proximity never implies a connection.">719 {hasGeo ? <NearbyInfrastructurePanel initial={f.nearbyInfrastructure} center={{ lat: f.lat!, lng: f.lng! }} excludeId={f.id} /> : <NotPublished what="Coordinates — nearby infrastructure" />}720 </Section>721722 {/* ------------------------------------------------------------ event history */}723 <Section id="events" title="Event history" className="scroll-mt-28" description="Detected changes to this record — status, capacity, dates, operator — with their source and significance." aside={<Link href={routes.live()} className="inline-flex items-center gap-1 text-[12.5px] text-ink-3 hover:text-ink">Live feed <ExternalLink className="size-3" /></Link>}>724 <EventList events={f.events} dense emptyTitle="No change detected yet" emptyDescription="Status, capacity and date changes will be listed here as connectors re-crawl the sources." />725 {f.sourceHistory.length > 0 && (726 <details className="mt-4 group">727 <summary className="cursor-pointer text-[12.5px] font-medium text-ink-2 hover:text-ink">728 Source observation log <span className="figure text-ink-3">{fmtInt(f.sourceHistory.length)}</span>729 </summary>730 <div className="mt-3">731 <SourceHistory items={[...f.sourceHistory].sort((a, b) => (a.date < b.date ? 1 : -1)).slice(0, 60)} />732 {f.sourceHistory.length > 60 && <p className="mt-2 text-[11.5px] text-ink-4">Showing the 60 most recent of {fmtInt(f.sourceHistory.length)} entries.</p>}733 </div>734 </details>735 )}736 </Section>737738 {/* ------------------------------------------------------------ source evidence */}739 <Section id="evidence" title="Source evidence" className="scroll-mt-28" description="Every observation behind this record: field, value, source, dates, confidence and method. “Shown” marks the winner backing the displayed value." aside={<span className="figure text-[12px] text-ink-3">{fmtInt(prov.length)} observations</span>}>740 <ProvenanceTable items={prov} />741 {claims.some((c) => /_usd$/.test(c.predicate)) && (742 <div className="mt-4">743 <div className="label-caps mb-2">Investment claims</div>744 <ClaimsTable claims={claims} kind="investment" />745 </div>746 )}747 <p className="mt-2 text-[11.5px] text-ink-4">748 {CONFIDENCE_LABEL[f.confidence]} overall — computed from the best source kind and the number of independent primary sources.{" "}749 <Link href={`${routes.methodology()}#confidence`} className="underline decoration-hair-2 hover:text-ink">750 How confidence is computed751 </Link>752 </p>753 </Section>754755 {/* ------------------------------------------------------------ data quality */}756 <Section id="quality" title="Data quality" className="scroll-mt-28">757 <DataQualityPanel q={dq} />758 </Section>759 </div>760761 <SourcesFooter sources={r.sources} updatedAt={f.updatedAt} />762 </Page>763 </EvidenceProvider>764 );765}766